package com.vortex.cloud.zhsw.xinyang.manager;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.vortex.cloud.lbs.enums.CoordtypeEnum;
import com.vortex.cloud.lbs.enums.ShapeTypeEnum;
import com.vortex.cloud.lbs.util.MapCoordinateConvertUtils;
import com.vortex.cloud.sdk.api.dto.ums.WorkElementDTO;
import com.vortex.cloud.sdk.api.service.ILbsSDKService;
import com.vortex.cloud.vfs.common.exception.VortexException;
import com.vortex.cloud.vfs.lite.base.dto.AbstractBaseDTO;
import com.vortex.cloud.vfs.lite.base.dto.GeometryDTO;
import com.vortex.cloud.vfs.lite.data.domain.AbstractBaseModel;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author
 */
@Service
public class LbsManagerService {
    @Autowired
    private ILbsSDKService lbsService;

    /**
     * DTO->Entity，GeometryDTO批量转Geometry
     *
     * @param entityList
     * @param dtoList
     */
    public <Entity extends AbstractBaseModel, DTO extends AbstractBaseDTO> void transferToEntityList(List<Entity> entityList, List<DTO> dtoList) {
        if (CollectionUtils.isEmpty(entityList) || CollectionUtils.isEmpty(dtoList)) {
            return;
        }

        Assert.isTrue(entityList.size() == dtoList.size(), "批量处理经纬度的2个集合参数必须长度相同");

        // 获取源和目标类中的Geometry/GeometryDTO类型的字段，并根据转化关系获取对应的get/set方法
        List<Method> getGeometryMethods = Lists.newArrayList();
        List<Method> setGeometryMethods = Lists.newArrayList();
        this.fetchGetAndSetGeometryMethods(dtoList.get(0), GeometryDTO.class, getGeometryMethods,
                entityList.get(0), Geometry.class, setGeometryMethods);

        // 获取DTO中com.vortex.cloud.vfs.lite.base.dto.GeometryDTO类型的数据
        List<GeometryDTO> geometryDTOList = Lists.newArrayList();
        for (Object dto : dtoList) {
            for (Method method : getGeometryMethods) {
                try {
                    GeometryDTO geometryDTO = (GeometryDTO) method.invoke(dto);
                    geometryDTOList.add(geometryDTO);
                } catch (Exception e) {
                    throw new VortexException("地图数据处理异常", e);
                }
            }
        }

        // 偏转
        List<Geometry> geometryList = this.transferToGeometryList(geometryDTOList);

        // 设置entity中org.locationtech.jts.geom.Geometry类型的数据
        int i = 0;
        for (Object entity : entityList) {
            for (Method method : setGeometryMethods) {
                try {
                    method.invoke(entity, geometryList.get(i++));
                } catch (Exception e) {
                    throw new VortexException("地图数据处理异常", e);
                }
            }
        }
    }

    /**
     * Entity->VO，Geometry批量转GeometryDTO
     *
     * @param entityList
     * @param voList
     * @param coordinateType
     */
    public <Entity extends AbstractBaseModel, VO extends AbstractBaseDTO> void transferFromEntityList(List<Entity> entityList, List<VO> voList, String coordinateType) {
        if (CollectionUtils.isEmpty(entityList) || CollectionUtils.isEmpty(voList)) {
            return;
        }

        Assert.isTrue(entityList.size() == voList.size(), "批量处理经纬度的2个集合参数必须长度相同");

        // 获取源和目标类中的Geometry/GeometryDTO类型的字段，并根据转化关系获取对应的get/set方法
        List<Method> getGeometryMethods = Lists.newArrayList();
        List<Method> setGeometryMethods = Lists.newArrayList();
        this.fetchGetAndSetGeometryMethods(entityList.get(0), Geometry.class, getGeometryMethods,
                voList.get(0), GeometryDTO.class, setGeometryMethods);

        // 获取entity中org.locationtech.jts.geom.Geometry类型的数据
        List<Geometry> geometryList = Lists.newArrayList();
        for (Object entity : entityList) {
            for (Method method : getGeometryMethods) {
                try {
                    Geometry geometry = (Geometry) method.invoke(entity);
                    geometryList.add(geometry);
                } catch (Exception e) {
                    throw new VortexException("地图数据处理异常", e);
                }
            }
        }

        // 偏转
        List<GeometryDTO> geometryDTOList = this.transferFromGeometryList(geometryList, coordinateType);

        // 设置VO中com.vortex.cloud.vfs.lite.base.dto.GeometryDTO类型的数据
        int i = 0;
        for (Object vo : voList) {
            for (Method method : setGeometryMethods) {
                try {
                    method.invoke(vo, geometryDTOList.get(i++));
                } catch (Exception e) {
                    throw new VortexException("地图数据处理异常", e);
                }
            }
        }
    }

    /**
     * DTO->Entity，GeometryDTO单个转Geometry
     *
     * @param entity
     * @param dto
     */
    public <Entity extends AbstractBaseModel, DTO extends AbstractBaseDTO> void transferToEntity(Entity entity, DTO dto) {
        // 获取源和目标类中的Geometry/GeometryDTO类型的字段，并根据转化关系获取对应的get/set方法
        List<Method> getGeometryMethods = Lists.newArrayList();
        List<Method> setGeometryMethods = Lists.newArrayList();
        this.fetchGetAndSetGeometryMethods(dto, GeometryDTO.class, getGeometryMethods,
                entity, Geometry.class, setGeometryMethods);

        // 获取DTO中com.vortex.cloud.vfs.lite.base.dto.GeometryDTO类型的数据
        List<GeometryDTO> geometryDTOList = Lists.newArrayList();
        for (Method method : getGeometryMethods) {
            try {
                GeometryDTO geometryDTO = (GeometryDTO) method.invoke(dto);
                geometryDTOList.add(geometryDTO);
            } catch (Exception e) {
                throw new VortexException("地图数据处理异常", e);
            }
        }

        // 偏转
        List<Geometry> geometryList = this.transferToGeometryList(geometryDTOList);

        // 设置entity中org.locationtech.jts.geom.Geometry类型的数据
        int i = 0;
        for (Method method : setGeometryMethods) {
            try {
                method.invoke(entity, geometryList.get(i++));
            } catch (Exception e) {
                throw new VortexException("地图数据处理异常", e);
            }
        }
    }

    /**
     * Entity->VO，Geometry单个转GeometryDTO
     *
     * @param entity
     * @param vo
     * @param coordinateType
     */
    public <Entity extends AbstractBaseModel, VO extends AbstractBaseDTO> void transferFromEntity(Entity entity, VO vo, String coordinateType) {
        // 获取源和目标类中的Geometry/GeometryDTO类型的字段，并根据转化关系获取对应的get/set方法
        List<Method> getGeometryMethods = Lists.newArrayList();
        List<Method> setGeometryMethods = Lists.newArrayList();
        this.fetchGetAndSetGeometryMethods(entity, Geometry.class, getGeometryMethods,
                vo, GeometryDTO.class, setGeometryMethods);

        // 获取entity中org.locationtech.jts.geom.Geometry类型的数据
        List<Geometry> geometryList = Lists.newArrayList();
        for (Method method : getGeometryMethods) {
            try {
                Geometry geometry = (Geometry) method.invoke(entity);
                geometryList.add(geometry);
            } catch (Exception e) {
                throw new VortexException("地图数据处理异常", e);
            }
        }

        // 偏转
        List<GeometryDTO> geometryDTOList = this.transferFromGeometryList(geometryList, coordinateType);

        // 设置VO中com.vortex.cloud.vfs.lite.base.dto.GeometryDTO类型的数据
        int i = 0;
        for (Method method : setGeometryMethods) {
            try {
                method.invoke(vo, geometryDTOList.get(i++));
            } catch (Exception e) {
                throw new VortexException("地图数据处理异常", e);
            }
        }
    }

    /**
     * GeometryDTO批量转Geometry
     *
     * @param geometryDTOList
     * @return
     */
    public List<Geometry> transferToGeometryList(List<GeometryDTO> geometryDTOList) {
        List<Geometry> geometryList = Lists.newArrayList();
        for (GeometryDTO geometryDTO : geometryDTOList) {
            geometryList.add(this.transferToGeometry(geometryDTO));
        }
        return geometryList;
    }

    /**
     * Geometry批量转GeometryDTO
     *
     * @param geometryList
     * @param coordinateType
     * @return
     */
    public List<GeometryDTO> transferFromGeometryList(List<Geometry> geometryList, String coordinateType) {
        List<GeometryDTO> geometryDTOList = Lists.newArrayList();
        for (Geometry geometry : geometryList) {
            geometryDTOList.add(this.transferFromGeometry(geometry, coordinateType));
        }
        return geometryDTOList;
    }

    /**
     * GeometryDTO单个转Geometry
     *
     * @param geometryDTO
     * @return
     */
    public Geometry transferToGeometry(GeometryDTO geometryDTO) {
        if (Objects.isNull(geometryDTO)) {
            return null;
        }

        String shapeType = geometryDTO.getShapeType();
        String coordinateType = geometryDTO.getCoordinateType();
        String lngLats = geometryDTO.getLngLats();
        if (StringUtils.isBlank(shapeType) || StringUtils.isBlank(coordinateType) || StringUtils.isBlank(lngLats)) {
            return null;
        }

        Geometry geometry = null;
        Coordinate[] coordinates = this.coordinateConvert(coordinateType, CoordtypeEnum.gps.getKey(), lngLats)
                .stream()
                .map(r -> new Coordinate(r[0], r[1]))
                .toArray(Coordinate[]::new);

        if (ArrayUtils.isNotEmpty(coordinates)) {
            ShapeTypeEnum shapeTypeEnum = ShapeTypeEnum.getByKey(geometryDTO.getShapeType());
            GeometryFactory geometryFactory = new GeometryFactory();

            switch (shapeTypeEnum) {
                case POINT:
                    geometry = geometryFactory.createPoint(coordinates[0]);
                    break;
                case LINE:
                case POLYLINE:
                    geometry = geometryFactory.createLineString(coordinates);
                    break;
                case POLYGON:
                    geometry = geometryFactory.createPolygon(this.constructCoordinates(coordinates));
                    break;
                default:
                    break;
            }
        }

        return geometry;
    }

    /**
     * Geometry单个转GeometryDTO
     *
     * @param geometry
     * @param coordinateType
     * @return
     */
    public GeometryDTO transferFromGeometry(Geometry geometry, String coordinateType) {
        if (Objects.isNull(geometry)) {
            return null;
        }

        String lngLats = Arrays.stream(geometry.getCoordinates())
                .map(coordinate -> coordinate.getX() + "," + coordinate.getY())
                .collect(Collectors.joining(";"));

        GeometryDTO geometryDTO = new GeometryDTO();
        geometryDTO.setCoordinateType(coordinateType);
        geometryDTO.setLngLats(this.coordinateConvert(CoordtypeEnum.gps.getKey(), coordinateType, lngLats)
                .stream()
                .map(r -> r[0] + "," + r[1])
                .collect(Collectors.joining(";")));

        switch (geometry.getGeometryType()) {
            case Geometry.TYPENAME_POINT:
                geometryDTO.setShapeType(ShapeTypeEnum.POINT.getKey());
                break;
            case Geometry.TYPENAME_LINESTRING:
                geometryDTO.setShapeType(ShapeTypeEnum.POLYLINE.getKey());
                break;
            case Geometry.TYPENAME_POLYGON:
                geometryDTO.setShapeType(ShapeTypeEnum.POLYGON.getKey());
                break;
            default:
                break;
        }

        return geometryDTO;
    }

    /**
     * 构造闭环的经纬度点（原因：多边形一般要求第一个经纬度和最后一个经纬度完全一样）
     *
     * @param coordinates
     * @return
     */
    private Coordinate[] constructCoordinates(Coordinate[] coordinates) {
        Coordinate first = coordinates[0];
        Coordinate last = coordinates[coordinates.length - 1];
        if (first.getX() == last.getX() && first.getY() == last.getY()) {
            return coordinates;
        } else {
            Coordinate[] newCoordinates = new Coordinate[coordinates.length + 1];
            System.arraycopy(coordinates, 0, newCoordinates, 0, coordinates.length);
            newCoordinates[coordinates.length] = new Coordinate(first.getX(), first.getY());
            return newCoordinates;
        }
    }

    /**
     * 偏转经纬度
     *
     * @param fromCoordinateType
     * @param toCoordinateType
     * @param lngLats
     * @return
     */
    private List<double[]> coordinateConvert(String fromCoordinateType, String toCoordinateType, String lngLats) {
        CoordtypeEnum fromCoordinateTypeEnum = CoordtypeEnum.getByKey(fromCoordinateType);
        CoordtypeEnum toCoordinateTypeEnum = CoordtypeEnum.getByKey(toCoordinateType);

        if (Objects.equals(fromCoordinateTypeEnum, toCoordinateTypeEnum)) {
            String[] lngLatList = lngLats.split(";");

            List<double[]> lngLatDoneList = Lists.newArrayList();
            for (String lngLat : lngLatList) {
                String[] lngLatArray = lngLat.split(",");
                Assert.isTrue(lngLatArray.length == 2, "经纬度只能为经度,纬度的格式");

                String lngString = lngLatArray[0];
                String latString = lngLatArray[1];
                Assert.isTrue(NumberUtils.isParsable(lngString) && NumberUtils.isParsable(latString), "经纬度只是能是数字格式");

                double lng = NumberUtils.toDouble(lngString);
                double lat = NumberUtils.toDouble(latString);

                lngLatDoneList.add(new double[]{lng, lat});
            }

            return lngLatDoneList;
        }
        boolean flag = (Objects.equals(fromCoordinateTypeEnum, CoordtypeEnum.baidu)
                || Objects.equals(fromCoordinateTypeEnum, CoordtypeEnum.gaode)
                || Objects.equals(fromCoordinateTypeEnum, CoordtypeEnum.gps))
                && (Objects.equals(toCoordinateTypeEnum, CoordtypeEnum.baidu)
                || Objects.equals(toCoordinateTypeEnum, CoordtypeEnum.gaode)
                || Objects.equals(toCoordinateTypeEnum, CoordtypeEnum.gps));
        if (flag) {
            String[] lngLatList = lngLats.split(";");

            List<double[]> lngLatDoneList = Lists.newArrayList();
            for (String lngLat : lngLatList) {
                lngLatDoneList.add(this.coordinateConvertByLocal(fromCoordinateTypeEnum, toCoordinateTypeEnum, lngLat));
            }

            return lngLatDoneList;
        } else {
            return this.coordinateConvertByRemote(fromCoordinateType, toCoordinateType, lngLats);
        }
    }

    /**
     * LBS算法偏转经纬度（仅针对三种常规坐标系的经纬度互相转换）
     *
     * @param fromCoordinateType
     * @param toCoordinateType
     * @param lngLat
     * @return
     */
    private double[] coordinateConvertByLocal(CoordtypeEnum fromCoordinateType, CoordtypeEnum toCoordinateType, String lngLat) {
        String[] lngLatArray = lngLat.split(",");
        Assert.isTrue(lngLatArray.length == 2, "经纬度只能为经度,纬度的格式");

        String lngString = lngLatArray[0];
        String latString = lngLatArray[1];
        Assert.isTrue(NumberUtils.isParsable(lngString) && NumberUtils.isParsable(latString), "经纬度只是能是数字格式");

        double lng = NumberUtils.toDouble(lngString);
        double lat = NumberUtils.toDouble(latString);

        double[] lngLatDone = null;

        if (Objects.equals(CoordtypeEnum.gps, fromCoordinateType) && Objects.equals(CoordtypeEnum.baidu, toCoordinateType)) {
            lngLatDone = MapCoordinateConvertUtils.wgs84Tobd09(lng, lat);
        } else if (Objects.equals(CoordtypeEnum.gps, fromCoordinateType) && Objects.equals(CoordtypeEnum.gaode, toCoordinateType)) {
            lngLatDone = MapCoordinateConvertUtils.wgs84Togcj02(lng, lat);
        } else if (Objects.equals(CoordtypeEnum.baidu, fromCoordinateType) && Objects.equals(CoordtypeEnum.gps, toCoordinateType)) {
            lngLatDone = MapCoordinateConvertUtils.bd09Towgs84(lng, lat);
        } else if (Objects.equals(CoordtypeEnum.baidu, fromCoordinateType) && Objects.equals(CoordtypeEnum.gaode, toCoordinateType)) {
            lngLatDone = MapCoordinateConvertUtils.bd09Togcj02(lng, lat);
        } else if (Objects.equals(CoordtypeEnum.gaode, fromCoordinateType) && Objects.equals(CoordtypeEnum.gps, toCoordinateType)) {
            lngLatDone = MapCoordinateConvertUtils.gcj02Towgs84(lng, lat);
        } else if (Objects.equals(CoordtypeEnum.gaode, fromCoordinateType) && Objects.equals(CoordtypeEnum.baidu, toCoordinateType)) {
            lngLatDone = MapCoordinateConvertUtils.gcj02Tobd09(lng, lat);
        }

        return lngLatDone;
    }

    /**
     * LBS服务偏转经纬度（仅针对非常规坐标系的经纬度）
     *
     * @param fromCoordinateType
     * @param toCoordinateType
     * @param lngLats
     * @return
     */
    private List<double[]> coordinateConvertByRemote(String fromCoordinateType, String toCoordinateType, String lngLats) {
        return lbsService.coordconvert2(lngLats, fromCoordinateType, toCoordinateType)
                .stream()
                .map(r -> new double[]{r.getLongitudeDone(), r.getLatitudeDone()})
                .collect(Collectors.toList());
    }

    /**
     * 获取源和目标类中的Geometry/GeometryDTO类型的字段，并根据转化关系获取对应的get/set方法
     *
     * @param source
     * @param targetFieldType
     * @param getMethods
     * @param target
     * @param sourceFieldType
     * @param setMethods
     */
    private <Source, Target> void fetchGetAndSetGeometryMethods(Source source, Class sourceFieldType, List<Method> getMethods,
                                                                Target target, Class targetFieldType, List<Method> setMethods) {
        if (Objects.isNull(source) || Objects.isNull(sourceFieldType) || Objects.isNull(getMethods)
                || Objects.isNull(target) || Objects.isNull(targetFieldType) || Objects.isNull(setMethods)) {
            return;
        }

        Class sourceClass = source.getClass();
        Class targetClass = target.getClass();

        // sourceClass中sourceFieldType类型的字段名称
        Set<String> sourceFieldNames = Arrays.stream(sourceClass.getDeclaredFields())
                .filter(r -> Objects.equals(r.getType(), sourceFieldType))
                .map(Field::getName)
                .collect(Collectors.toSet());
        // targetClass中targetFieldType类型的字段名称
        Set<String> targetFieldNames = Arrays.stream(targetClass.getDeclaredFields())
                .filter(r -> Objects.equals(r.getType(), targetFieldType))
                .map(Field::getName)
                .collect(Collectors.toSet());
        // 求交集并且首字母大写
        Set<String> fieldNames = Sets.intersection(sourceFieldNames, targetFieldNames)
                .stream()
                .map(r -> StringUtils.capitalize(r))
                .collect(Collectors.toSet());
        // sourceClass中get方法
        getMethods.addAll(fieldNames.stream()
                .sorted()
                .map(r -> {
                    String getMethodName = "get" + r;
                    try {
                        return sourceClass.getMethod(getMethodName, null);
                    } catch (Exception e) {
                        throw new VortexException("地图数据处理异常", e);
                    }
                })
                .collect(Collectors.toList()));
        // targetClass中set方法
        setMethods.addAll(fieldNames.stream()
                .sorted()
                .map(r -> {
                    String setMethodName = "set" + r;
                    try {
                        return targetClass.getMethod(setMethodName, targetFieldType);
                    } catch (Exception e) {
                        throw new VortexException("地图数据处理异常", e);
                    }
                })
                .collect(Collectors.toList()));
    }

    public String getAddress(String lngLats,String coordType) {
        List<WorkElementDTO> elements = org.apache.commons.compress.utils.Lists.newArrayList();
        WorkElementDTO element = new WorkElementDTO();
        element.setParams(lngLats);
        elements.add(element);
        lbsService.reverseGeocoding(elements, coordType, record -> record.getParams(), (record, address) -> record.setDescription(address));
        return elements.get(0).getDescription();
    }
}
